/****************************************************************************
 * Copyright (c) 2009 Remy Chi Jian Suen and others.
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * Contributors:
 *     Remy Chi Jian Suen - initial API and implementation
 *
 * SPDX-License-Identifier: EPL-2.0
 *****************************************************************************/
package org.eclipse.ecf.tests.provider.datashare.nio;

import java.net.InetSocketAddress;

import junit.framework.TestCase;

import org.eclipse.ecf.core.IContainer;
import org.eclipse.ecf.core.identity.ID;
import org.eclipse.ecf.core.identity.IDFactory;
import org.eclipse.ecf.core.util.ECFException;
import org.eclipse.ecf.datashare.IChannel;
import org.eclipse.ecf.datashare.IChannelContainerAdapter;
import org.eclipse.ecf.datashare.IChannelListener;
import org.eclipse.ecf.datashare.events.IChannelConnectEvent;
import org.eclipse.ecf.datashare.events.IChannelDisconnectEvent;
import org.eclipse.ecf.datashare.events.IChannelEvent;
import org.eclipse.ecf.datashare.events.IChannelMessageEvent;

public class NIODatashareTest extends TestCase {

	private static final String LOCALHOST = "127.0.0.1"; //$NON-NLS-1$
	// private static final String LOCALHOST = "localhost"; //$NON-NLS-1$
	// private static final String LOCALHOST = "0.0.0.0"; //$NON-NLS-1$

	private static final String CHANNEL_NAME = "channel"; //$NON-NLS-1$

	private Object waitObject = new Object();

	private Exception exception;

	private IContainer containerA = new ContainerImpl();
	private IContainer containerB = new ContainerImpl();

	private ConcreteNIODatashareContainer channelContainerA;
	private ConcreteNIODatashareContainer channelContainerB;

	private ConcreteNIOChannel channelA;
	private ConcreteNIOChannel channelB;

	private static void assertEquals(byte[] expected, byte[] actual) {
		// don't use Arrays.equals(byte[], byte[]) to get more accurate failure
		// information
		assertNotNull(expected);
		assertNotNull(actual);

		assertEquals(expected.length, actual.length);

		for (int i = 0; i < expected.length; i++) {
			assertEquals(expected[i], actual[i]);
		}
	}

	private static ConcreteNIOChannel createChannel(
			IChannelContainerAdapter channelContainer) throws ECFException {
		return createChannel(channelContainer, null);
	}

	private static ConcreteNIOChannel createChannel(
			IChannelContainerAdapter channelContainer, IChannelListener listener)
			throws ECFException {
		return (ConcreteNIOChannel) channelContainer.createChannel(IDFactory
				.getDefault().createStringID(CHANNEL_NAME), listener, null);
	}

	protected void setUp() throws Exception {
		super.setUp();

		channelContainerA = new ConcreteNIODatashareContainer(containerA);
		channelContainerB = new ConcreteNIODatashareContainer(containerB);

		exception = null;
	}

	protected void tearDown() throws Exception {
		containerA.disconnect();
		containerB.disconnect();

		channelContainerA = null;
		channelContainerB = null;

		channelA = null;
		channelB = null;

		// just to make sure we don't have any pending threads
		synchronized (waitObject) {
			waitObject.notifyAll();
		}

		super.tearDown();
	}

	private void waitForCompletion(long timeout) throws Exception {
		synchronized (waitObject) {
			waitObject.wait(timeout);
		}

		if (exception != null) {
			throw exception;
		}
	}

	private void send(IChannel channel, ID receiver, byte[] data) {
		try {
			channel.sendMessage(receiver, data);
		} catch (ECFException e) {
			exception = e;

			synchronized (waitObject) {
				waitObject.notify();
			}
		}
	}

	public void testChannelConnectEvent() throws Exception {
		final ID[] eventIds = new ID[2];

		channelA = createChannel(channelContainerA, new IChannelListener() {
			public void handleChannelEvent(IChannelEvent e) {
				if (e instanceof IChannelConnectEvent) {
					IChannelConnectEvent event = (IChannelConnectEvent) e;
					eventIds[0] = event.getChannelID();
					eventIds[1] = event.getTargetID();
				}
			}
		});

		containerA.connect(null, null);

		assertEquals(channelA.getID(), eventIds[0]);
		assertEquals(containerA.getConnectedID(), eventIds[1]);
	}

	/**
	 * Test that while the container fires events, the datashare implementation
	 * is ignoring them successfully as it does not have a listener and is not
	 * throwing runtime exceptions.
	 */
	public void testChannelConnectEventWithoutListeners() throws Exception {
		channelA = createChannel(channelContainerA);
		containerA.connect(null, null);
	}

	public void testChannelDisconnectEvent() throws Exception {
		final ID[] eventIds = new ID[2];

		channelA = createChannel(channelContainerA, new IChannelListener() {
			public void handleChannelEvent(IChannelEvent e) {
				if (e instanceof IChannelDisconnectEvent) {
					IChannelDisconnectEvent event = (IChannelDisconnectEvent) e;
					eventIds[0] = event.getChannelID();
					eventIds[1] = event.getTargetID();
				}
			}
		});

		containerA.disconnect();

		assertEquals(channelA.getID(), eventIds[0]);
		// technically, getConnectedID() should return null when a container has
		// disconnected, but anyway...
		assertEquals(containerA.getConnectedID(), eventIds[1]);
	}

	/**
	 * Test that while the container fires events, the datashare implementation
	 * is ignoring them successfully as it does not have a listener and is not
	 * throwing runtime exceptions.
	 */
	public void testChannelDisconnectEventWithoutListeners() throws Exception {
		channelA = createChannel(channelContainerA, null);
		containerA.disconnect();
	}

	public void testOneWaySend() throws Exception {
		final byte[][] actual = new byte[1][];

		channelA = createChannel(channelContainerA);

		int targetPort = channelA.getPort();

		channelB = createChannel(channelContainerB, new IChannelListener() {
			public void handleChannelEvent(IChannelEvent event) {
				if (event instanceof IChannelMessageEvent) {
					actual[0] = ((IChannelMessageEvent) event).getData();

					synchronized (waitObject) {
						waitObject.notify();
					}
				}
			}
		});

		byte[] expected = { 1, 2, 3 };

		channelA.sendMessage(containerB.getConnectedID(), expected);

		channelContainerB.enqueue(new InetSocketAddress(LOCALHOST, targetPort));

		waitForCompletion(5000);

		assertEquals(expected, actual[0]);
	}

	public void testOneWaySend16k() throws Exception {
		final byte[][] actual = new byte[1][];

		channelA = createChannel(channelContainerA);

		int targetPort = channelA.getPort();

		channelB = createChannel(channelContainerB, new IChannelListener() {
			public void handleChannelEvent(IChannelEvent event) {
				if (event instanceof IChannelMessageEvent) {
					actual[0] = ((IChannelMessageEvent) event).getData();

					synchronized (waitObject) {
						waitObject.notify();
					}
				}
			}
		});

		byte[] expected = new byte[16384];
		for (int i = 0; i < expected.length; i++) {
			expected[i] = (byte) (i % 128);
		}

		channelA.sendMessage(containerB.getConnectedID(), expected);

		channelContainerB.enqueue(new InetSocketAddress(LOCALHOST, targetPort));

		waitForCompletion(10000);

		assertEquals(expected, actual[0]);
	}

	public void testSendAndReply() throws Exception {
		final byte[] expected1 = { 1, 2, 3 };
		final byte[] expected2 = { 4, 5, 6 };

		final byte[][] actual = new byte[2][];

		channelA = createChannel(channelContainerA, new IChannelListener() {
			public void handleChannelEvent(IChannelEvent event) {
				if (event instanceof IChannelMessageEvent) {
					actual[1] = ((IChannelMessageEvent) event).getData();

					synchronized (waitObject) {
						waitObject.notify();
					}
				}
			}
		});

		int targetPort = channelA.getPort();

		channelB = createChannel(channelContainerB, new IChannelListener() {
			public void handleChannelEvent(IChannelEvent event) {
				if (event instanceof IChannelMessageEvent) {
					actual[0] = ((IChannelMessageEvent) event).getData();

					send(channelB, containerA.getConnectedID(), expected2);
				}
			}
		});

		channelA.sendMessage(containerB.getConnectedID(), expected1);

		channelContainerB.enqueue(new InetSocketAddress(LOCALHOST, targetPort));

		waitForCompletion(5000);

		assertEquals(expected1, actual[0]);
		assertEquals(expected2, actual[1]);
	}
}
